package org.durandj.ystream.server;

import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.channels.SelectionKey;
import java.security.InvalidParameterException;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import org.durandj.ystream.server.message.InvalidMessageContentsException;
import org.durandj.ystream.server.message.Message;
import org.durandj.ystream.server.message.MessageHandler;
import org.durandj.ystream.server.network.Client;
import org.durandj.ystream.server.network.Opcode;
import org.durandj.ystream.server.network.Packet;
import org.durandj.ystream.server.util.ByteBufferUtils;
import org.durandj.ystream.server.util.InetAddressUtils;
import org.durandj.ystream.server.util.Timer;

/**
 * Manages connections to the clients. This class will keep
 * track of any and all client connections to the server.
 * 
 * @author durandj
 * @version 1.0
 *
 */
public class ClientManager extends MessageHandler
{
	/**
	 * The list of clients currently connected.
	 */
	private Map<SelectionKey, Client> clients;
	
	/**
	 * The client manager's listeners.
	 */
	private LinkedList<ClientManagerListener> listeners;
	
	/**
	 * The maximum number of clients allowed.
	 */
	private int max;
	
	/**
	 * The flag that keeps the client manager alive.
	 */
	private boolean isAlive;
	
	/**
	 * The thread of execution.
	 */
	private Thread thread;
	
	/**
	 * Creates a client manager.
	 * 
	 * @param maxConnections - the maximum number of clients that can be connected
	 */
	public ClientManager(int maxConnections)
	{
		if(maxConnections <= 0)
			throw new InvalidParameterException("Must give a valid number of max connections.");
		
		clients = new HashMap<SelectionKey, Client>();
		max = maxConnections;
		isAlive = true;
		listeners = new LinkedList<ClientManagerListener>();
		
		thread = new Thread(new Runnable() {
			@Override
			public void run()
			{
				while(isAlive)
				{
					if(noMessages())
						waitForMessage();
					
					handleMessage(nextMessage());
					
					new Timer(100);
				}
				
				// We need to do some cleanup before the client manager can stop.
				for(Client client : clients.values())
					if(!client.isDirty())
						client.killWatchdog();
				clients.clear();
			}
		}, "Client Manager");
		thread.start();
	}
	
	/**
	 * Adds a new client to the server.
	 * 
	 * @param client - the new client
	 */
	public void addClient(SelectionKey key, Client client)
	{
		if(key == null)
			throw new NullPointerException("Must give a valid selection key.");
		
		if(client == null)
			throw new NullPointerException("Must give a valid client.");
		
		if(clients.size() == max)
		{
			log.info("Connection to client refused. Too many clients.");
			return;
			// TODO: More should be done here such as notifying that client.
		}
		
		clients.put(key, client);
		client.getPacketSender().sendPacket(new Packet(Packet.Priority.HIGH,
		                                               Opcode.NOP,
		                                               InetAddressUtils.getStringAddress(client.getClientAddress()),
		                                               InetAddressUtils.getStringAddress(Server.getServer().getNetworkDaemon().getServerAddress())));
		
		notifyListeners();
	}
	
	/**
	 * Gets the reference to the client based on the address of the client. If the client is not connected to the server
	 * then this returns null.
	 * 
	 * @param address - the address of the client
	 * @return a client reference or null
	 */
	// TODO: This method should probably be phased out...
	public Client getClient(InetAddress address)
	{
		if(address == null)
			throw new NullPointerException("Must give a valid address.");
		
		for(Client client : clients.values())
		{
			if(client.getClientAddress().equals(address))
				return client;
		}
		
		return null;
	}
	
	/**
	 * Gets the reference to the client based on the selection key.
	 * 
	 * @param key - the selection key
	 * @return a client referece
	 */
	public Client getClient(SelectionKey key)
	{
		if(key == null)
			throw new NullPointerException("Must give a valid selection key.");
		
		return clients.get(key);
	}
	
	/**
	 * A list of all the currently connected clients.
	 * 
	 * @return the currently connected clients
	 */
	public Collection<Client> getClients()
	{
		return clients.values();
	}
	
	/**
	 * Disconnects the client based on their address.
	 * 
	 * @param address - the address of the client
	 */
	private void disconnectClient(InetAddress address)
	{
		Client client = getClient(address);
		
		if(client == null)
			return;
		
		client.setDirty(true);
		
		client.killWatchdog();
		clients.remove(Server.getServer().getNetworkDaemon().getSelectionKey(client));
		
		notifyListeners();
	}
	
	/**
	 * Adds a new listener.
	 * 
	 * @param listener - the client manager listener
	 */
	public void addListener(ClientManagerListener listener)
	{
		if(listener == null)
			throw new NullPointerException("Must give a valid listener object.");
		
		listeners.add(listener);
	}
	
	/**
	 * Removes a listener from the listener list.
	 * 
	 * @param listener - the listener
	 */
	public void removeListener(ClientManagerListener listener)
	{
		if(listener == null)
			throw new NullPointerException("Must give a valid listener object.");
		
		listeners.remove(listener);
	}
	
	/**
	 * Notifies all the observers of a change in the client listing.
	 */
	private void notifyListeners()
	{
		for(ClientManagerListener listener : listeners)
			listener.clientsChanged();
	}

	/*
	 * (non-Javadoc)
	 * @see org.durandj.ystream.server.message.MessageHandler#handleMessage(org.durandj.ystream.server.message.Message)
	 */
	@Override
	protected void handleMessage(Message msg)
	{
		switch(msg.getID())
		{
		case KILL:
			isAlive = false;
			break;
			
		case DISCONNECT_CLIENT:
		{
			// One of the clients needs to be disconnected so we first need
			// to know which client that is. The address of the client is given
			// by the message so lets make sure that it's there and that the
			// value is valid for what we are expecting.
			if(msg.getContents() == null)
				throw new InvalidMessageContentsException("Must give the client that is being disconnected.");
			
			// We know that there is some kind of data given by the message
			// so let's do some verification of it.
			String address = ByteBufferUtils.getStringFromByteBuffer(msg.getContents());
			
			if(address.isEmpty())
				throw new InvalidMessageContentsException("Must give a valid client.");
			
			// Ok we now have a valid client so let's disconnect them.
			// TODO: We should send a notification to the client.
			try
			{
				disconnectClient(InetAddress.getByName(address));
			}
			catch(UnknownHostException e)
			{}
			break;
		}
			
		case TIMER_EXPIRED:
		{
			// One of the client timers have expired so we need to disconnect them.
			// To be able to determine which client was the offender we need to
			// check the contents of the message so let's take a second to check
			// that there are indeed contents in this message.
			if(msg.getContents() == null)
				throw new InvalidMessageContentsException("Must give the client that is being disconnected.");
			
			// We know the message has some contents let's start trying to pull the information we need.
			// Specifically we need to know what client had their watchdog timer expire.
			String clientAddress = ByteBufferUtils.getStringFromByteBuffer(msg.getContents());
			InetAddress inetAddress = null;
			try
			{
				inetAddress = InetAddress.getByName(clientAddress);
			}
			catch(UnknownHostException e)
			{
				log.warning("The specified client does not exist.");
				return;
			}
			
			log.info("A client watchdog timed out. Address: " + inetAddress);
			disconnectClient(inetAddress);
			
			break;
		}
			
		default:
			log.warning("Unhandled message!\n" +
						"\tID: " + msg.getID() + "\n" +
						"\tSender: " + msg.getSender().getClass().getSimpleName() + "\n");
			break;
		}
	}
}
